import test from "ava";
import { escapeRegExp, isString, omit, sortBy } from "lodash-es";
import * as td from "testdouble";
import { spy, stub } from "sinon";
import { WritableStreamBuffer } from "stream-buffers";
import AggregateError from "aggregate-error";
import SemanticReleaseError from "@semantic-release/error";
import { COMMIT_EMAIL, COMMIT_NAME, SECRET_REPLACEMENT } from "../lib/definitions/constants.js";
import {
  gitAddNote,
  gitCheckout,
  gitCommits,
  gitGetNote,
  gitHead as getGitHead,
  gitPush,
  gitRemoteTagHead,
  gitRepo,
  gitShallowClone,
  gitTagHead,
  gitTagVersion,
  merge,
  mergeFf,
  rebase,
} from "./helpers/git-utils.js";
import pluginNoop from "./fixtures/plugin-noop.cjs";

test.beforeEach((t) => {
  // Stub the logger functions
  t.context.log = spy();
  t.context.error = spy();
  t.context.success = spy();
  t.context.warn = spy();
  t.context.logger = {
    log: t.context.log,
    error: t.context.error,
    success: t.context.success,
    warn: t.context.warn,
    scope: () => t.context.logger,
  };
});

test.afterEach.always((t) => {
  td.reset();
});

test.serial("Plugins are called with expected values", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  let commits = await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["next"] }), "v1.0.0", { cwd });
  commits = (await gitCommits(["Second"], { cwd })).concat(commits);
  await gitCheckout("next", true, { cwd });
  await gitPush(repositoryUrl, "next", { cwd });
  await gitCheckout("master", false, { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const lastRelease = {
    version: "1.0.0",
    gitHead: commits[commits.length - 1].hash,
    gitTag: "v1.0.0",
    name: "v1.0.0",
    channels: ["next"],
  };
  const nextRelease = {
    name: "v1.1.0",
    type: "minor",
    version: "1.1.0",
    gitHead: await getGitHead({ cwd }),
    gitTag: "v1.1.0",
    channel: null,
  };
  const notes1 = "Release notes 1";
  const notes2 = "Release notes 2";
  const notes3 = "Release notes 3";
  const verifyConditions1 = stub().resolves();
  const verifyConditions2 = stub().resolves();
  const analyzeCommits = stub().resolves(nextRelease.type);
  const verifyRelease = stub().resolves();
  const generateNotes1 = stub().resolves(notes1);
  const generateNotes2 = stub().resolves(notes2);
  const generateNotes3 = stub().resolves(notes3);
  const release1 = { name: "Release 1", url: "https://release1.com" };
  const release2 = { name: "Release 2", url: "https://release2.com" };
  const addChannel = stub().resolves(release1);
  const prepare = stub().resolves();
  const publish = stub().resolves(release2);
  const success = stub().resolves();
  const env = {};
  const config = {
    branches: [{ name: "master" }, { name: "next" }],
    repositoryUrl,
    originalRepositoryURL: repositoryUrl,
    globalOpt: "global",
    tagFormat: `v\${version}`,
  };
  const branches = [
    {
      channel: undefined,
      name: "master",
      range: ">=1.0.0",
      accept: ["patch", "minor", "major"],
      tags: [{ channels: ["next"], gitTag: "v1.0.0", version: "1.0.0" }],
      type: "release",
      main: true,
    },
    {
      channel: "next",
      name: "next",
      range: ">=1.0.0",
      accept: ["patch", "minor", "major"],
      tags: [{ channels: ["next"], gitTag: "v1.0.0", version: "1.0.0" }],
      type: "release",
      main: false,
    },
  ];
  const branch = branches[0];
  const options = {
    ...config,
    plugins: false,
    verifyConditions: [verifyConditions1, verifyConditions2],
    analyzeCommits,
    verifyRelease,
    addChannel,
    generateNotes: [generateNotes1, generateNotes2, generateNotes3],
    prepare,
    publish: [publish, pluginNoop],
    success,
  };
  const envCiResults = { branch: "master", isCi: true, isPr: false };

  const releases = [
    {
      ...omit(lastRelease, "channels"),
      ...release1,
      type: "major",
      version: "1.0.0",
      channel: null,
      gitTag: "v1.0.0",
      notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
      pluginName: "[Function: functionStub]",
    },
    {
      ...nextRelease,
      ...release2,
      notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
      pluginName: "[Function: functionStub]",
    },
    { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: "[Function: noop]" },
  ];

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  const envCi = (await td.replaceEsm("env-ci")).default;
  td.when(envCi({ env, cwd })).thenReturn(envCiResults);
  const semanticRelease = (await import("../index.js")).default;
  const result = await semanticRelease(options, {
    cwd,
    env,
    stdout: new WritableStreamBuffer(),
    stderr: new WritableStreamBuffer(),
  });

  t.is(verifyConditions1.callCount, 1);
  t.deepEqual(verifyConditions1.args[0][0], config);
  t.deepEqual(verifyConditions1.args[0][1].cwd, cwd);
  t.deepEqual(verifyConditions1.args[0][1].options, options);
  t.deepEqual(verifyConditions1.args[0][1].branch, branch);
  t.deepEqual(verifyConditions1.args[0][1].branches, branches);
  t.deepEqual(verifyConditions1.args[0][1].logger, t.context.logger);
  t.deepEqual(verifyConditions1.args[0][1].envCi, envCiResults);
  t.is(verifyConditions2.callCount, 1);
  t.deepEqual(verifyConditions2.args[0][0], config);
  t.deepEqual(verifyConditions2.args[0][1].cwd, cwd);
  t.deepEqual(verifyConditions2.args[0][1].options, options);
  t.deepEqual(verifyConditions2.args[0][1].branch, branch);
  t.deepEqual(verifyConditions2.args[0][1].branches, branches);
  t.deepEqual(verifyConditions2.args[0][1].logger, t.context.logger);
  t.deepEqual(verifyConditions2.args[0][1].envCi, envCiResults);

  t.is(generateNotes1.callCount, 2);
  t.is(generateNotes2.callCount, 2);
  t.is(generateNotes3.callCount, 2);

  t.deepEqual(generateNotes1.args[0][0], config);
  t.deepEqual(generateNotes1.args[0][1].options, options);
  t.deepEqual(generateNotes1.args[0][1].branch, branch);
  t.deepEqual(generateNotes1.args[0][1].branches, branches);
  t.deepEqual(generateNotes1.args[0][1].logger, t.context.logger);
  t.deepEqual(generateNotes1.args[0][1].lastRelease, {});
  t.deepEqual(generateNotes1.args[0][1].commits[0].hash, commits[1].hash);
  t.deepEqual(generateNotes1.args[0][1].commits[0].message, commits[1].message);
  t.deepEqual(generateNotes1.args[0][1].nextRelease, {
    ...omit(lastRelease, "channels"),
    type: "major",
    version: "1.0.0",
    channel: null,
    gitTag: "v1.0.0",
    name: "v1.0.0",
  });
  t.deepEqual(generateNotes2.args[0][1].envCi, envCiResults);

  t.deepEqual(generateNotes2.args[0][0], config);
  t.deepEqual(generateNotes2.args[0][1].options, options);
  t.deepEqual(generateNotes2.args[0][1].branch, branch);
  t.deepEqual(generateNotes2.args[0][1].branches, branches);
  t.deepEqual(generateNotes2.args[0][1].logger, t.context.logger);
  t.deepEqual(generateNotes2.args[0][1].lastRelease, {});
  t.deepEqual(generateNotes2.args[0][1].commits[0].hash, commits[1].hash);
  t.deepEqual(generateNotes2.args[0][1].commits[0].message, commits[1].message);
  t.deepEqual(generateNotes2.args[0][1].nextRelease, {
    ...omit(lastRelease, "channels"),
    type: "major",
    version: "1.0.0",
    channel: null,
    gitTag: "v1.0.0",
    name: "v1.0.0",
    notes: notes1,
  });
  t.deepEqual(generateNotes2.args[0][1].envCi, envCiResults);

  t.deepEqual(generateNotes3.args[0][0], config);
  t.deepEqual(generateNotes3.args[0][1].options, options);
  t.deepEqual(generateNotes3.args[0][1].branch, branch);
  t.deepEqual(generateNotes3.args[0][1].branches, branches);
  t.deepEqual(generateNotes3.args[0][1].logger, t.context.logger);
  t.deepEqual(generateNotes3.args[0][1].lastRelease, {});
  t.deepEqual(generateNotes3.args[0][1].commits[0].hash, commits[1].hash);
  t.deepEqual(generateNotes3.args[0][1].commits[0].message, commits[1].message);
  t.deepEqual(generateNotes3.args[0][1].nextRelease, {
    ...omit(lastRelease, "channels"),
    type: "major",
    version: "1.0.0",
    channel: null,
    gitTag: "v1.0.0",
    name: "v1.0.0",
    notes: `${notes1}\n\n${notes2}`,
  });
  t.deepEqual(generateNotes3.args[0][1].envCi, envCiResults);

  branch.tags.push({
    version: "1.0.0",
    channel: null,
    gitTag: "v1.0.0",
    gitHead: commits[commits.length - 1].hash,
  });

  t.is(addChannel.callCount, 1);
  t.deepEqual(addChannel.args[0][0], config);
  t.deepEqual(addChannel.args[0][1].options, options);
  t.deepEqual(addChannel.args[0][1].branch, branch);
  t.deepEqual(addChannel.args[0][1].branches, branches);
  t.deepEqual(addChannel.args[0][1].logger, t.context.logger);
  t.deepEqual(addChannel.args[0][1].lastRelease, {});
  t.deepEqual(addChannel.args[0][1].currentRelease, { ...lastRelease, type: "major" });
  t.deepEqual(addChannel.args[0][1].nextRelease, {
    ...omit(lastRelease, "channels"),
    type: "major",
    version: "1.0.0",
    channel: null,
    gitTag: "v1.0.0",
    name: "v1.0.0",
    notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
  });
  t.deepEqual(addChannel.args[0][1].commits[0].hash, commits[1].hash);
  t.deepEqual(addChannel.args[0][1].commits[0].message, commits[1].message);
  t.deepEqual(addChannel.args[0][1].envCi, envCiResults);

  t.is(analyzeCommits.callCount, 1);
  t.deepEqual(analyzeCommits.args[0][0], config);
  t.deepEqual(analyzeCommits.args[0][1].options, options);
  t.deepEqual(analyzeCommits.args[0][1].branch, branch);
  t.deepEqual(analyzeCommits.args[0][1].branches, branches);
  t.deepEqual(analyzeCommits.args[0][1].logger, t.context.logger);
  t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease);
  t.deepEqual(analyzeCommits.args[0][1].commits[0].hash, commits[0].hash);
  t.deepEqual(analyzeCommits.args[0][1].commits[0].message, commits[0].message);
  t.deepEqual(analyzeCommits.args[0][1].envCi, envCiResults);

  t.is(verifyRelease.callCount, 1);
  t.deepEqual(verifyRelease.args[0][0], config);
  t.deepEqual(verifyRelease.args[0][1].options, options);
  t.deepEqual(verifyRelease.args[0][1].branch, branch);
  t.deepEqual(verifyRelease.args[0][1].branches, branches);
  t.deepEqual(verifyRelease.args[0][1].logger, t.context.logger);
  t.deepEqual(verifyRelease.args[0][1].lastRelease, lastRelease);
  t.deepEqual(verifyRelease.args[0][1].commits[0].hash, commits[0].hash);
  t.deepEqual(verifyRelease.args[0][1].commits[0].message, commits[0].message);
  t.deepEqual(verifyRelease.args[0][1].nextRelease, nextRelease);
  t.deepEqual(verifyRelease.args[0][1].envCi, envCiResults);

  t.deepEqual(generateNotes1.args[1][0], config);
  t.deepEqual(generateNotes1.args[1][1].options, options);
  t.deepEqual(generateNotes1.args[1][1].branch, branch);
  t.deepEqual(generateNotes1.args[1][1].branches, branches);
  t.deepEqual(generateNotes1.args[1][1].logger, t.context.logger);
  t.deepEqual(generateNotes1.args[1][1].lastRelease, lastRelease);
  t.deepEqual(generateNotes1.args[1][1].commits[0].hash, commits[0].hash);
  t.deepEqual(generateNotes1.args[1][1].commits[0].message, commits[0].message);
  t.deepEqual(generateNotes1.args[1][1].nextRelease, nextRelease);
  t.deepEqual(generateNotes1.args[1][1].envCi, envCiResults);

  t.deepEqual(generateNotes2.args[1][0], config);
  t.deepEqual(generateNotes2.args[1][1].options, options);
  t.deepEqual(generateNotes2.args[1][1].branch, branch);
  t.deepEqual(generateNotes2.args[1][1].branches, branches);
  t.deepEqual(generateNotes2.args[1][1].logger, t.context.logger);
  t.deepEqual(generateNotes2.args[1][1].lastRelease, lastRelease);
  t.deepEqual(generateNotes2.args[1][1].commits[0].hash, commits[0].hash);
  t.deepEqual(generateNotes2.args[1][1].commits[0].message, commits[0].message);
  t.deepEqual(generateNotes2.args[1][1].nextRelease, { ...nextRelease, notes: notes1 });
  t.deepEqual(generateNotes2.args[1][1].envCi, envCiResults);

  t.deepEqual(generateNotes3.args[1][0], config);
  t.deepEqual(generateNotes3.args[1][1].options, options);
  t.deepEqual(generateNotes3.args[1][1].branch, branch);
  t.deepEqual(generateNotes3.args[1][1].branches, branches);
  t.deepEqual(generateNotes3.args[1][1].logger, t.context.logger);
  t.deepEqual(generateNotes3.args[1][1].lastRelease, lastRelease);
  t.deepEqual(generateNotes3.args[1][1].commits[0].hash, commits[0].hash);
  t.deepEqual(generateNotes3.args[1][1].commits[0].message, commits[0].message);
  t.deepEqual(generateNotes3.args[1][1].nextRelease, { ...nextRelease, notes: `${notes1}\n\n${notes2}` });
  t.deepEqual(generateNotes3.args[1][1].envCi, envCiResults);

  t.is(prepare.callCount, 1);
  t.deepEqual(prepare.args[0][0], config);
  t.deepEqual(prepare.args[0][1].options, options);
  t.deepEqual(prepare.args[0][1].branch, branch);
  t.deepEqual(prepare.args[0][1].branches, branches);
  t.deepEqual(prepare.args[0][1].logger, t.context.logger);
  t.deepEqual(prepare.args[0][1].lastRelease, lastRelease);
  t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
  t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
  t.deepEqual(prepare.args[0][1].nextRelease, { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}` });
  t.deepEqual(prepare.args[0][1].envCi, envCiResults);

  t.is(publish.callCount, 1);
  t.deepEqual(publish.args[0][0], config);
  t.deepEqual(publish.args[0][1].options, options);
  t.deepEqual(publish.args[0][1].branch, branch);
  t.deepEqual(publish.args[0][1].branches, branches);
  t.deepEqual(publish.args[0][1].logger, t.context.logger);
  t.deepEqual(publish.args[0][1].lastRelease, lastRelease);
  t.deepEqual(publish.args[0][1].commits[0].hash, commits[0].hash);
  t.deepEqual(publish.args[0][1].commits[0].message, commits[0].message);
  t.deepEqual(publish.args[0][1].nextRelease, { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}` });
  t.deepEqual(publish.args[0][1].envCi, envCiResults);

  t.is(success.callCount, 2);
  t.deepEqual(success.args[0][0], config);
  t.deepEqual(success.args[0][1].options, options);
  t.deepEqual(success.args[0][1].branch, branch);
  t.deepEqual(success.args[0][1].branches, branches);
  t.deepEqual(success.args[0][1].logger, t.context.logger);
  t.deepEqual(success.args[0][1].lastRelease, {});
  t.deepEqual(success.args[0][1].commits[0].hash, commits[1].hash);
  t.deepEqual(success.args[0][1].commits[0].message, commits[1].message);
  t.deepEqual(success.args[0][1].nextRelease, {
    ...omit(lastRelease, "channels"),
    type: "major",
    version: "1.0.0",
    channel: null,
    gitTag: "v1.0.0",
    name: "v1.0.0",
    notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
  });
  t.deepEqual(success.args[0][1].releases, [releases[0]]);
  t.deepEqual(success.args[0][1].envCi, envCiResults);

  t.deepEqual(success.args[1][0], config);
  t.deepEqual(success.args[1][1].options, options);
  t.deepEqual(success.args[1][1].branch, branch);
  t.deepEqual(success.args[1][1].branches, branches);
  t.deepEqual(success.args[1][1].logger, t.context.logger);
  t.deepEqual(success.args[1][1].lastRelease, lastRelease);
  t.deepEqual(success.args[1][1].commits[0].hash, commits[0].hash);
  t.deepEqual(success.args[1][1].commits[0].message, commits[0].message);
  t.deepEqual(success.args[1][1].nextRelease, { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}` });
  t.deepEqual(success.args[1][1].releases, [releases[1], releases[2]]);
  t.deepEqual(success.args[1][1].envCi, envCiResults);

  t.deepEqual(result, {
    lastRelease,
    commits: [{ ...commits[0], gitTags: "(HEAD -> master, next)" }],
    nextRelease: { ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}` },
    releases,
  });

  // Verify the tag has been created on the local and remote repo and reference the gitHead
  t.is(await gitTagHead(nextRelease.gitTag, { cwd }), nextRelease.gitHead);
  t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, { cwd }), nextRelease.gitHead);

  // Verify the author/commiter name and email have been set
  t.is(env.GIT_AUTHOR_NAME, COMMIT_NAME);
  t.is(env.GIT_AUTHOR_EMAIL, COMMIT_EMAIL);
  t.is(env.GIT_COMMITTER_NAME, COMMIT_NAME);
  t.is(env.GIT_COMMITTER_EMAIL, COMMIT_EMAIL);
});

test.serial("Use custom tag format", async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["First"], { cwd });
  await gitTagVersion("test-1.0.0", undefined, { cwd });
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const nextRelease = {
    name: "test-2.0.0",
    type: "major",
    version: "2.0.0",
    gitHead: await getGitHead({ cwd }),
    gitTag: "test-2.0.0",
  };
  const notes = "Release notes";
  const config = { branches: "master", repositoryUrl, globalOpt: "global", tagFormat: `test-\${version}` };
  const options = {
    ...config,
    verifyConditions: stub().resolves(),
    analyzeCommits: stub().resolves(nextRelease.type),
    verifyRelease: stub().resolves(),
    generateNotes: stub().resolves(notes),
    addChannel: stub().resolves(),
    prepare: stub().resolves(),
    publish: stub().resolves(),
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  t.truthy(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );

  // Verify the tag has been created on the local and remote repo and reference the gitHead
  t.is(await gitTagHead(nextRelease.gitTag, { cwd }), nextRelease.gitHead);
  t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, { cwd }), nextRelease.gitHead);
});

test.serial("Use new gitHead, and recreate release notes if a prepare plugin create a commit", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  let commits = await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  // Add new commits to the master branch
  commits = (await gitCommits(["Second"], { cwd })).concat(commits);
  await gitPush(repositoryUrl, "master", { cwd });

  const nextRelease = {
    name: "v2.0.0",
    type: "major",
    version: "2.0.0",
    gitHead: await getGitHead({ cwd }),
    gitTag: "v2.0.0",
    channel: null,
  };
  const notes = "Release notes";

  const generateNotes = stub().resolves(notes);
  const prepare1 = stub().callsFake(async () => {
    commits = (await gitCommits(["Third"], { cwd })).concat(commits);
  });
  const prepare2 = stub().resolves();
  const publish = stub().resolves();
  const options = {
    branches: ["master"],
    repositoryUrl,
    verifyConditions: stub().resolves(),
    analyzeCommits: stub().resolves(nextRelease.type),
    verifyRelease: stub().resolves(),
    generateNotes,
    addChannel: stub().resolves(),
    prepare: [prepare1, prepare2],
    publish,
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;

  t.truthy(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );

  t.is(generateNotes.callCount, 2);
  t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
  t.is(prepare1.callCount, 1);
  t.deepEqual(prepare1.args[0][1].nextRelease, { ...nextRelease, notes });

  nextRelease.gitHead = await getGitHead({ cwd });

  t.deepEqual(generateNotes.args[1][1].nextRelease, { ...nextRelease, notes });
  t.is(prepare2.callCount, 1);
  t.deepEqual(prepare2.args[0][1].nextRelease, { ...nextRelease, notes });

  t.is(publish.callCount, 1);
  t.deepEqual(publish.args[0][1].nextRelease, { ...nextRelease, notes });

  // Verify the tag has been created on the local and remote repo and reference the last gitHead
  t.is(await gitTagHead(nextRelease.gitTag, { cwd }), commits[0].hash);
  t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, { cwd }), commits[0].hash);
});

test.serial("Make a new release when a commit is forward-ported to an upper branch", async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["feat: initial release"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: [null, "1.0.x"] }), "v1.0.0", { cwd });
  await gitCheckout("1.0.x", true, { cwd });
  await gitCommits(["fix: fix on maintenance version 1.0.x"], { cwd });
  await gitTagVersion("v1.0.1", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["1.0.x"] }), "v1.0.1", { cwd });
  await gitPush("origin", "1.0.x", { cwd });
  await gitCheckout("master", false, { cwd });
  await gitCommits(["feat: new feature on master"], { cwd });
  await gitTagVersion("v1.1.0", undefined, { cwd });
  await merge("1.0.x", { cwd });
  await gitPush("origin", "master", { cwd });

  const verifyConditions = stub().resolves();
  const verifyRelease = stub().resolves();
  const addChannel = stub().resolves();
  const prepare = stub().resolves();
  const publish = stub().resolves();
  const success = stub().resolves();

  const config = { branches: [{ name: "1.0.x" }, { name: "master" }], repositoryUrl, tagFormat: `v\${version}` };
  const options = {
    ...config,
    verifyConditions,
    verifyRelease,
    addChannel,
    prepare,
    publish,
    success,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  t.truthy(await semanticRelease(options, { cwd, env: {}, stdout: { write: () => {} }, stderr: { write: () => {} } }));

  t.is(addChannel.callCount, 0);
  t.is(publish.callCount, 1);
  // The release 1.1.1, triggered by the forward-port of "fix: fix on maintenance version 1.0.x" has been published from master
  t.is(publish.args[0][1].nextRelease.version, "1.1.1");
  t.is(success.callCount, 1);
});

test.serial("Publish a pre-release version", async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["feat: initial commit"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitPush(repositoryUrl, "master", { cwd });
  await gitCheckout("beta", true, { cwd });
  await gitCommits(["feat: a feature"], { cwd });
  await gitPush(repositoryUrl, "beta", { cwd });

  const config = { branches: ["master", { name: "beta", prerelease: true }], repositoryUrl };
  const options = {
    ...config,
    verifyConditions: stub().resolves(),
    verifyRelease: stub().resolves(),
    generateNotes: stub().resolves(""),
    addChannel: false,
    prepare: stub().resolves(),
    publish: stub().resolves(),
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  const env = {};
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  const envCi = (await td.replaceEsm("env-ci")).default;
  td.when(envCi({ env, cwd })).thenReturn({ isCi: true, branch: "beta", isPr: false });
  const semanticRelease = (await import("../index.js")).default;
  let { releases } = await semanticRelease(options, {
    cwd,
    env: {},
    stdout: { write: () => {} },
    stderr: { write: () => {} },
  });

  t.is(releases.length, 1);
  t.is(releases[0].version, "1.1.0-beta.1");
  t.is(releases[0].gitTag, "v1.1.0-beta.1");
  t.is(await gitGetNote("v1.1.0-beta.1", { cwd }), '{"channels":["beta"]}');

  await gitCommits(["fix: a fix"], { cwd });
  ({ releases } = await semanticRelease(options, {
    cwd,
    env,
    stdout: { write: () => {} },
    stderr: { write: () => {} },
  }));

  t.is(releases.length, 1);
  t.is(releases[0].version, "1.1.0-beta.2");
  t.is(releases[0].gitTag, "v1.1.0-beta.2");
  t.is(await gitGetNote("v1.1.0-beta.2", { cwd }), '{"channels":["beta"]}');
});

test.serial("Publish releases from different branch on the same channel", async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["feat: initial commit"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitPush(repositoryUrl, "master", { cwd });
  await gitCheckout("next-major", true, { cwd });
  await gitPush(repositoryUrl, "next-major", { cwd });
  await gitCheckout("next", true, { cwd });
  await gitCommits(["feat: a feature"], { cwd });
  await gitPush(repositoryUrl, "next", { cwd });

  const config = {
    branches: ["master", { name: "next", channel: false }, { name: "next-major", channel: false }],
    repositoryUrl,
  };
  const addChannel = stub().resolves({});
  const options = {
    ...config,
    verifyConditions: stub().resolves(),
    verifyRelease: stub().resolves(),
    generateNotes: stub().resolves(""),
    addChannel,
    prepare: stub().resolves(),
    publish: stub().resolves(),
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  const env = {};
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  const envCi = (await td.replaceEsm("env-ci")).default;
  td.when(envCi({ env, cwd })).thenReturn({ isCi: true, branch: "next", isPr: false });
  let semanticRelease = (await import("../index.js")).default;
  let { releases } = await semanticRelease(options, {
    cwd,
    env,
    stdout: { write: () => {} },
    stderr: { write: () => {} },
  });

  t.is(releases.length, 1);
  t.is(releases[0].version, "1.1.0");
  t.is(releases[0].gitTag, "v1.1.0");

  await gitCommits(["fix: a fix"], { cwd });
  ({ releases } = await semanticRelease(options, {
    cwd,
    env,
    stdout: { write: () => {} },
    stderr: { write: () => {} },
  }));

  t.is(releases.length, 1);
  t.is(releases[0].version, "1.1.1");
  t.is(releases[0].gitTag, "v1.1.1");

  await gitCheckout("master", false, { cwd });
  await merge("next", { cwd });
  await gitPush("origin", "master", { cwd });

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  semanticRelease = (await import("../index.js")).default;

  t.falsy(await semanticRelease(options, { cwd, env: {}, stdout: { write: () => {} }, stderr: { write: () => {} } }));
  t.is(addChannel.callCount, 0);
});

test.serial("Publish pre-releases the same channel as regular releases", async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["feat: initial commit"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitPush(repositoryUrl, "master", { cwd });
  await gitCheckout("beta", true, { cwd });
  await gitCommits(["feat: a feature"], { cwd });
  await gitPush(repositoryUrl, "beta", { cwd });

  const config = {
    branches: ["master", { name: "beta", channel: false, prerelease: true }],
    repositoryUrl,
  };
  const options = {
    ...config,
    verifyConditions: stub().resolves(),
    verifyRelease: stub().resolves(),
    generateNotes: stub().resolves(""),
    addChannel: false,
    prepare: stub().resolves(),
    publish: stub().resolves(),
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  const env = {};
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  const envCi = (await td.replaceEsm("env-ci")).default;
  td.when(envCi({ cwd, env })).thenReturn({ isCi: true, branch: "beta", isPr: false });
  const semanticRelease = (await import("../index.js")).default;
  let { releases } = await semanticRelease(options, {
    cwd,
    env,
    stdout: { write: () => {} },
    stderr: { write: () => {} },
  });

  t.is(releases.length, 1);
  t.is(releases[0].version, "1.1.0-beta.1");
  t.is(releases[0].gitTag, "v1.1.0-beta.1");

  await gitCommits(["fix: a fix"], { cwd });
  ({ releases } = await semanticRelease(options, {
    cwd,
    env,
    stdout: { write: () => {} },
    stderr: { write: () => {} },
  }));

  t.is(releases.length, 1);
  t.is(releases[0].version, "1.1.0-beta.2");
  t.is(releases[0].gitTag, "v1.1.0-beta.2");
});

test.serial("Do not add pre-releases to a different channel", async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["feat: initial release"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: [null, "beta"] }), "v1.0.0", { cwd });
  await gitCheckout("beta", true, { cwd });
  await gitCommits(["feat: breaking change/n/nBREAKING CHANGE: break something"], { cwd });
  await gitTagVersion("v2.0.0-beta.1", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["beta"] }), "v2.0.0-beta.1", { cwd });
  await gitCommits(["fix: a fix"], { cwd });
  await gitTagVersion("v2.0.0-beta.2", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["beta"] }), "v2.0.0-beta.2", { cwd });
  await gitPush("origin", "beta", { cwd });
  await gitCheckout("master", false, { cwd });
  await merge("beta", { cwd });
  await gitPush("origin", "master", { cwd });

  const verifyConditions = stub().resolves();
  const verifyRelease = stub().resolves();
  const generateNotes = stub().resolves("Release notes");
  const release1 = { name: "Release 1", url: "https://release1.com" };
  const addChannel = stub().resolves(release1);
  const prepare = stub().resolves();
  const publish = stub().resolves();
  const success = stub().resolves();

  const config = {
    branches: [{ name: "master" }, { name: "beta", prerelease: "beta" }],
    repositoryUrl,
    tagFormat: `v\${version}`,
  };

  const options = {
    ...config,
    verifyConditions,
    verifyRelease,
    addChannel,
    generateNotes,
    prepare,
    publish,
    success,
  };

  const env = {};
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  const envCi = (await td.replaceEsm("env-ci")).default;
  td.when(envCi({ cwd, env })).thenReturn({ isCi: true, branch: "master", isPr: false });
  const semanticRelease = (await import("../index.js")).default;
  t.truthy(await semanticRelease(options, { cwd, env, stdout: { write: () => {} }, stderr: { write: () => {} } }));

  t.is(addChannel.callCount, 0);
});

async function addChannelMacro(t, mergeFunction) {
  const { cwd, repositoryUrl } = await gitRepo(true);
  const commits = await gitCommits(["feat: initial release"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: [null, "next"] }), "v1.0.0", { cwd });
  await gitCheckout("next", true, { cwd });
  commits.push(...(await gitCommits(["feat: breaking change/n/nBREAKING CHANGE: break something"], { cwd })));
  await gitTagVersion("v2.0.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["next"] }), "v2.0.0", { cwd });

  commits.push(...(await gitCommits(["fix: a fix"], { cwd })));
  await gitTagVersion("v2.0.1", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["next"] }), "v2.0.1", { cwd });
  commits.push(...(await gitCommits(["feat: a feature"], { cwd })));
  await gitTagVersion("v2.1.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["next"] }), "v2.1.0", { cwd });
  await gitPush("origin", "next", { cwd });
  await gitCheckout("master", false, { cwd });
  // Merge all commits but last one from next to master
  await mergeFunction("next~1", { cwd });
  await gitPush("origin", "master", { cwd });

  const notes = "Release notes";
  const verifyConditions = stub().resolves();
  const verifyRelease = stub().resolves();
  const generateNotes = stub().resolves(notes);
  const release1 = { name: "Release 1", url: "https://release1.com" };
  const addChannel1 = stub().resolves(release1);
  const addChannel2 = stub().resolves();
  const prepare = stub().resolves();
  const publish = stub().resolves();
  const success = stub().resolves();

  const config = {
    branches: [
      { name: "master", channel: "latest" },
      { name: "next", channel: "next" },
    ],
    repositoryUrl,
    tagFormat: `v\${version}`,
  };
  const options = {
    ...config,
    verifyConditions,
    verifyRelease,
    addChannel: [addChannel1, addChannel2],
    generateNotes,
    prepare,
    publish,
    success,
  };
  const nextRelease = {
    name: "v2.0.1",
    type: "patch",
    version: "2.0.1",
    channel: "latest",
    gitTag: "v2.0.1",
    gitHead: commits[2].hash,
  };

  const env = {};
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  const envCi = (await td.replaceEsm("env-ci")).default;
  td.when(envCi({ env, cwd })).thenReturn({ isCi: true, branch: "master", isPr: false });
  const semanticRelease = (await import("../index.js")).default;
  const result = await semanticRelease(options, { cwd, env, stdout: { write: () => {} }, stderr: { write: () => {} } });

  t.deepEqual(result.releases, [
    { ...nextRelease, ...release1, notes, pluginName: "[Function: functionStub]" },
    { ...nextRelease, notes, pluginName: "[Function: functionStub]" },
  ]);

  // Verify the tag has been created on the local and remote repo and reference
  t.is(await gitTagHead(nextRelease.gitTag, { cwd }), nextRelease.gitHead);
  t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, { cwd }), nextRelease.gitHead);
}

addChannelMacro.title = (providedTitle) => `Add version to a channel after a merge (${providedTitle})`;

test.serial("fast-forward", addChannelMacro, mergeFf);
test.serial("non fast-forward", addChannelMacro, merge);
test.serial("rebase", addChannelMacro, rebase);

test.serial('Call all "success" plugins even if one errors out', async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  // Add new commits to the master branch
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const nextRelease = {
    name: "v2.0.0",
    type: "major",
    version: "2.0.0",
    gitHead: await getGitHead({ cwd }),
    gitTag: "v2.0.0",
    channel: null,
  };
  const notes = "Release notes";
  const verifyConditions1 = stub().resolves();
  const verifyConditions2 = stub().resolves();
  const analyzeCommits = stub().resolves(nextRelease.type);
  const generateNotes = stub().resolves(notes);
  const release = { name: "Release", url: "https://release.com" };
  const publish = stub().resolves(release);
  const success1 = stub().rejects();
  const success2 = stub().resolves();
  const config = {
    branches: [{ name: "master" }],
    repositoryUrl,
    globalOpt: "global",
    tagFormat: `v\${version}`,
  };
  const options = {
    ...config,
    verifyConditions: [verifyConditions1, verifyConditions2],
    analyzeCommits,
    generateNotes,
    addChannel: stub().resolves(),
    prepare: stub().resolves(),
    publish,
    success: [success1, success2],
  };

  const env = {};
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  const envCi = (await td.replaceEsm("env-ci")).default;
  td.when(envCi({ cwd, env })).thenReturn({ isCi: true, branch: "master", isPr: false });
  const semanticRelease = (await import("../index.js")).default;

  await t.throwsAsync(
    semanticRelease(options, { cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
  );

  t.is(success1.callCount, 1);
  t.deepEqual(success1.args[0][0], config);
  t.deepEqual(success1.args[0][1].releases, [
    { ...nextRelease, ...release, notes, pluginName: "[Function: functionStub]" },
  ]);

  t.is(success2.callCount, 1);
  t.deepEqual(success2.args[0][1].releases, [
    { ...nextRelease, ...release, notes, pluginName: "[Function: functionStub]" },
  ]);
});

test.serial('Log all "verifyConditions" errors', async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const error1 = new Error("error 1");
  const error2 = new SemanticReleaseError("error 2", "ERR2");
  const error3 = new SemanticReleaseError("error 3", "ERR3");
  const fail = stub().resolves();
  const config = {
    branches: [{ name: "master" }],
    repositoryUrl,
    originalRepositoryURL: repositoryUrl,
    tagFormat: `v\${version}`,
  };
  const options = {
    ...config,
    plugins: false,
    verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)],
    fail,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  const errors = [
    ...(
      await t.throwsAsync(
        semanticRelease(options, {
          cwd,
          env: {},
          stdout: new WritableStreamBuffer(),
          stderr: new WritableStreamBuffer(),
        })
      )
    ).errors,
  ];

  t.deepEqual(sortBy(errors, ["message"]), sortBy([error1, error2, error3], ["message"]));
  t.true(t.context.error.calledWith("An error occurred while running semantic-release: %O", error1));
  t.true(t.context.error.calledWith("ERR2 error 2"));
  t.true(t.context.error.calledWith("ERR3 error 3"));
  t.true(t.context.error.calledAfter(t.context.log));
  t.is(fail.callCount, 1);
  t.deepEqual(fail.args[0][0], config);
  t.deepEqual(fail.args[0][1].options, options);
  t.deepEqual(fail.args[0][1].logger, t.context.logger);
  t.deepEqual(fail.args[0][1].errors, [error2, error3]);
});

test.serial('Log all "verifyRelease" errors', async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  // Add new commits to the master branch
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const error1 = new SemanticReleaseError("error 1", "ERR1");
  const error2 = new SemanticReleaseError("error 2", "ERR2");
  const fail = stub().resolves();
  const config = { branches: [{ name: "master" }], repositoryUrl, tagFormat: `v\${version}` };
  const options = {
    ...config,
    verifyConditions: stub().resolves(),
    analyzeCommits: stub().resolves("major"),
    verifyRelease: [stub().rejects(error1), stub().rejects(error2)],
    fail,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  const errors = [
    ...(
      await t.throwsAsync(
        semanticRelease(options, {
          cwd,
          env: {},
          stdout: new WritableStreamBuffer(),
          stderr: new WritableStreamBuffer(),
        })
      )
    ).errors,
  ];

  t.deepEqual(sortBy(errors, ["message"]), sortBy([error1, error2], ["message"]));
  t.true(t.context.error.calledWith("ERR1 error 1"));
  t.true(t.context.error.calledWith("ERR2 error 2"));
  t.is(fail.callCount, 1);
  t.deepEqual(fail.args[0][0], config);
  t.deepEqual(fail.args[0][1].errors, [error1, error2]);
});

test.serial("Dry-run skips addChannel, prepare, publish and success", async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["First"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: [null, "next"] }), "v1.0.0", { cwd });
  await gitCommits(["Second"], { cwd });
  await gitTagVersion("v1.1.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["next"] }), "v1.1.0", { cwd });

  await gitPush(repositoryUrl, "master", { cwd });
  await gitCheckout("next", true, { cwd });
  await gitPush("origin", "next", { cwd });

  const verifyConditions = stub().resolves();
  const analyzeCommits = stub().resolves("minor");
  const verifyRelease = stub().resolves();
  const generateNotes = stub().resolves();
  const addChannel = stub().resolves();
  const prepare = stub().resolves();
  const publish = stub().resolves();
  const success = stub().resolves();

  const options = {
    dryRun: true,
    branches: ["master", "next"],
    repositoryUrl,
    verifyConditions,
    analyzeCommits,
    verifyRelease,
    generateNotes,
    addChannel,
    prepare,
    publish,
    success,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  t.truthy(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );

  t.not(t.context.warn.args[0][0], "This run was not triggered in a known CI environment, running in dry-run mode.");
  t.is(verifyConditions.callCount, 1);
  t.is(analyzeCommits.callCount, 1);
  t.is(verifyRelease.callCount, 1);
  t.is(generateNotes.callCount, 2);
  t.is(addChannel.callCount, 0);
  t.true(
    t.context.warn.calledWith(`Skip step "addChannel" of plugin "[Function: ${addChannel.name}]" in dry-run mode`)
  );
  t.is(prepare.callCount, 0);
  t.true(t.context.warn.calledWith(`Skip step "prepare" of plugin "[Function: ${prepare.name}]" in dry-run mode`));
  t.is(publish.callCount, 0);
  t.true(t.context.warn.calledWith(`Skip step "publish" of plugin "[Function: ${publish.name}]" in dry-run mode`));
  t.is(success.callCount, 0);
  t.true(t.context.warn.calledWith(`Skip step "success" of plugin "[Function: ${success.name}]" in dry-run mode`));
});

test.serial("Dry-run skips fail", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  // Add new commits to the master branch
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const error1 = new SemanticReleaseError("error 1", "ERR1");
  const error2 = new SemanticReleaseError("error 2", "ERR2");
  const fail = stub().resolves();

  const options = {
    dryRun: true,
    branches: ["master"],
    repositoryUrl,
    verifyConditions: [stub().rejects(error1), stub().rejects(error2)],
    fail,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  const errors = [
    ...(
      await t.throwsAsync(
        semanticRelease(options, {
          cwd,
          env: {},
          stdout: new WritableStreamBuffer(),
          stderr: new WritableStreamBuffer(),
        })
      )
    ).errors,
  ];

  t.deepEqual(sortBy(errors, ["message"]), sortBy([error1, error2], ["message"]));
  t.true(t.context.error.calledWith("ERR1 error 1"));
  t.true(t.context.error.calledWith("ERR2 error 2"));
  t.is(fail.callCount, 0);
  t.true(t.context.warn.calledWith(`Skip step "fail" of plugin "[Function: ${fail.name}]" in dry-run mode`));
});

test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  // Add new commits to the master branch
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const nextRelease = {
    name: "v2.0.0",
    type: "major",
    version: "2.0.0",
    gitHead: await getGitHead({ cwd }),
    gitTag: "v2.0.0",
    channel: undefined,
  };
  const notes = "Release notes";

  const verifyConditions = stub().resolves();
  const analyzeCommits = stub().resolves(nextRelease.type);
  const verifyRelease = stub().resolves();
  const generateNotes = stub().resolves(notes);
  const publish = stub().resolves();
  const success = stub().resolves();

  const options = {
    dryRun: false,
    branches: ["master"],
    repositoryUrl,
    verifyConditions,
    analyzeCommits,
    verifyRelease,
    generateNotes,
    addChannel: stub().resolves(),
    prepare: stub().resolves(),
    publish,
    success,
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: false, branch: "master" }));
  const semanticRelease = (await import("../index.js")).default;
  t.truthy(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );

  t.true(t.context.warn.calledWith("This run was not triggered in a known CI environment, running in dry-run mode."));
  t.is(verifyConditions.callCount, 1);
  t.is(analyzeCommits.callCount, 1);
  t.is(verifyRelease.callCount, 1);
  t.is(generateNotes.callCount, 1);
  t.is(publish.callCount, 0);
  t.is(success.callCount, 0);
});

test.serial('Dry-run does not print changelog if "generateNotes" return "undefined"', async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  // Add new commits to the master branch
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const nextRelease = { type: "major", version: "2.0.0", gitHead: await getGitHead({ cwd }), gitTag: "v2.0.0" };
  const analyzeCommits = stub().resolves(nextRelease.type);
  const generateNotes = stub().resolves();

  const options = {
    dryRun: true,
    branches: ["master"],
    repositoryUrl,
    verifyConditions: false,
    analyzeCommits,
    verifyRelease: false,
    generateNotes,
    prepare: false,
    publish: false,
    success: false,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  t.truthy(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );

  t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ["Release note for version 2.0.0:"]);
});

test.serial('Allow local releases with "noCi" option', async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  // Add new commits to the master branch
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const nextRelease = {
    name: "v2.0.0",
    type: "major",
    version: "2.0.0",
    gitHead: await getGitHead({ cwd }),
    gitTag: "v2.0.0",
    channel: undefined,
  };
  const notes = "Release notes";

  const verifyConditions = stub().resolves();
  const analyzeCommits = stub().resolves(nextRelease.type);
  const verifyRelease = stub().resolves();
  const generateNotes = stub().resolves(notes);
  const publish = stub().resolves();
  const success = stub().resolves();

  const options = {
    noCi: true,
    branches: ["master"],
    repositoryUrl,
    verifyConditions,
    analyzeCommits,
    verifyRelease,
    generateNotes,
    addChannel: stub().resolves(),
    prepare: stub().resolves(),
    publish,
    success,
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: false, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  t.truthy(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );

  t.not(t.context.log.args[0][0], "This run was not triggered in a known CI environment, running in dry-run mode.");
  t.not(
    t.context.log.args[0][0],
    "This run was triggered by a pull request and therefore a new version won't be published."
  );
  t.is(verifyConditions.callCount, 1);
  t.is(analyzeCommits.callCount, 1);
  t.is(verifyRelease.callCount, 1);
  t.is(generateNotes.callCount, 1);
  t.is(publish.callCount, 1);
  t.is(success.callCount, 1);
});

test.serial(
  'Accept "undefined" value returned by "generateNotes" and "false" by "publish" and "addChannel"',
  async (t) => {
    const { cwd, repositoryUrl } = await gitRepo(true);
    await gitCommits(["First"], { cwd });
    await gitTagVersion("v1.0.0", undefined, { cwd });
    await gitAddNote(JSON.stringify({ channels: [null, "next"] }), "v1.0.0", { cwd });
    await gitCommits(["Second"], { cwd });
    await gitTagVersion("v1.1.0", undefined, { cwd });
    await gitAddNote(JSON.stringify({ channels: ["next"] }), "v1.1.0", { cwd });
    await gitPush(repositoryUrl, "master", { cwd });
    await gitCheckout("next", true, { cwd });
    await gitPush("origin", "next", { cwd });
    await gitCheckout("master", false, { cwd });

    const nextRelease = {
      name: "v1.2.0",
      type: "minor",
      version: "1.2.0",
      gitHead: await getGitHead({ cwd }),
      gitTag: "v1.2.0",
      channel: null,
    };
    const analyzeCommits = stub().resolves(nextRelease.type);
    const verifyRelease = stub().resolves();
    const generateNotes1 = stub().resolves();
    const notes2 = "Release notes 2";
    const generateNotes2 = stub().resolves(notes2);
    const publish = stub().resolves(false);
    const addChannel = stub().resolves(false);
    const success = stub().resolves();

    const options = {
      branches: ["master", "next"],
      repositoryUrl,
      verifyConditions: stub().resolves(),
      analyzeCommits,
      verifyRelease,
      generateNotes: [generateNotes1, generateNotes2],
      addChannel,
      prepare: stub().resolves(),
      publish,
      success,
      fail: stub().resolves(),
    };

    await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
    await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
    const semanticRelease = (await import("../index.js")).default;
    t.truthy(
      await semanticRelease(options, {
        cwd,
        env: {},
        stdout: new WritableStreamBuffer(),
        stderr: new WritableStreamBuffer(),
      })
    );

    t.is(analyzeCommits.callCount, 1);
    t.is(verifyRelease.callCount, 1);
    t.is(generateNotes1.callCount, 2);
    t.is(generateNotes2.callCount, 2);
    t.is(addChannel.callCount, 1);
    t.is(publish.callCount, 1);
    t.is(success.callCount, 2);
    t.deepEqual(publish.args[0][1].nextRelease, { ...nextRelease, notes: notes2 });
    t.deepEqual(success.args[0][1].releases, [{ pluginName: "[Function: functionStub]" }]);
    t.deepEqual(success.args[1][1].releases, [{ pluginName: "[Function: functionStub]" }]);
  }
);

test.serial("Returns false if triggered by a PR", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", prBranch: "patch-1", isPr: true }));
  const semanticRelease = (await import("../index.js")).default;

  t.false(
    await semanticRelease(
      { cwd, repositoryUrl },
      { cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() }
    )
  );
  t.is(
    t.context.log.args[t.context.log.args.length - 1][0],
    "This run was triggered by a pull request and therefore a new version won't be published."
  );
});

test.serial(
  'Throws "EINVALIDNEXTVERSION" if next release is out of range of the current maintenance branch',
  async (t) => {
    const { cwd, repositoryUrl } = await gitRepo(true);
    await gitCommits(["feat: initial commit"], { cwd });
    await gitTagVersion("v1.0.0", undefined, { cwd });
    await gitAddNote(JSON.stringify({ channels: [null, "1.x"] }), "v1.0.0", { cwd });
    await gitCheckout("1.x", true, { cwd });
    await gitPush("origin", "1.x", { cwd });
    await gitCheckout("master", false, { cwd });
    await gitCommits(["feat: new feature on master"], { cwd });
    await gitTagVersion("v1.1.0", undefined, { cwd });
    await gitCheckout("1.x", false, { cwd });
    await gitCommits(["feat: feature on maintenance version 1.x"], { cwd });
    await gitPush("origin", "master", { cwd });

    const verifyConditions = stub().resolves();
    const verifyRelease = stub().resolves();
    const addChannel = stub().resolves();
    const prepare = stub().resolves();
    const publish = stub().resolves();
    const success = stub().resolves();

    const config = {
      branches: [{ name: "1.x" }, { name: "master" }],
      repositoryUrl,
      tagFormat: `v\${version}`,
    };
    const options = {
      ...config,
      verifyConditions,
      verifyRelease,
      addChannel,
      prepare,
      publish,
      success,
    };

    const env = {};
    await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
    const envCi = (await td.replaceEsm("env-ci")).default;
    td.when(envCi({ cwd, env })).thenReturn({ isCi: true, branch: "1.x", isPr: false });
    const semanticRelease = (await import("../index.js")).default;

    const error = await t.throwsAsync(
      semanticRelease(options, { cwd, env, stdout: { write: () => {} }, stderr: { write: () => {} } })
    );

    t.is(error.code, "EINVALIDNEXTVERSION");
    t.is(error.name, "SemanticReleaseError");
    t.is(error.message, "The release `1.1.0` on branch `1.x` cannot be published as it is out of range.");
    t.regex(error.details, /A valid branch could be `master`./);
  }
);

test.serial('Throws "EINVALIDNEXTVERSION" if next release is out of range of the current release branch', async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["feat: initial commit"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitCheckout("next", true, { cwd });
  await gitCommits(["feat: new feature on next"], { cwd });
  await gitTagVersion("v1.1.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: ["next"] }), "v1.1.0", { cwd });
  await gitPush("origin", "next", { cwd });
  await gitCheckout("next-major", true, { cwd });
  await gitPush("origin", "next-major", { cwd });
  await gitCheckout("master", false, { cwd });
  await gitCommits(["feat: new feature on master", "fix: new fix on master"], { cwd });
  await gitPush("origin", "master", { cwd });

  const verifyConditions = stub().resolves();
  const verifyRelease = stub().resolves();
  const addChannel = stub().resolves();
  const prepare = stub().resolves();
  const publish = stub().resolves();
  const success = stub().resolves();

  const config = {
    branches: [{ name: "master" }, { name: "next" }, { name: "next-major" }],
    repositoryUrl,
    tagFormat: `v\${version}`,
  };
  const options = {
    ...config,
    verifyConditions,
    verifyRelease,
    addChannel,
    prepare,
    publish,
    success,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;

  const error = await t.throwsAsync(
    semanticRelease(options, { cwd, env: {}, stdout: { write: () => {} }, stderr: { write: () => {} } })
  );

  t.is(error.code, "EINVALIDNEXTVERSION");
  t.is(error.name, "SemanticReleaseError");
  t.is(error.message, "The release `1.1.0` on branch `master` cannot be published as it is out of range.");
  t.regex(error.details, /A valid branch could be `next` or `next-major`./);
});

test.serial('Throws "EINVALIDMAINTENANCEMERGE" if merge an out of range release in a maintenance branch', async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["First"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: [null, "1.1.x"] }), "v1.0.0", { cwd });
  await gitCommits(["Second"], { cwd });
  await gitTagVersion("v1.1.0", undefined, { cwd });
  await gitAddNote(JSON.stringify({ channels: [null, "1.1.x"] }), "v1.1.0", { cwd });
  await gitCheckout("1.1.x", "master", { cwd });
  await gitPush("origin", "1.1.x", { cwd });
  await gitCheckout("master", false, { cwd });
  await gitCommits(["Third"], { cwd });
  await gitTagVersion("v1.1.1", undefined, { cwd });
  await gitCommits(["Fourth"], { cwd });
  await gitTagVersion("v1.2.0", undefined, { cwd });
  await gitPush("origin", "master", { cwd });
  await gitCheckout("1.1.x", false, { cwd });
  await merge("master", { cwd });
  await gitPush("origin", "1.1.x", { cwd });

  const notes = "Release notes";
  const verifyConditions = stub().resolves();
  const analyzeCommits = stub().resolves();
  const verifyRelease = stub().resolves();
  const generateNotes = stub().resolves(notes);
  const addChannel = stub().resolves();
  const prepare = stub().resolves();
  const publish = stub().resolves();
  const success = stub().resolves();
  const fail = stub().resolves();

  const config = { branches: [{ name: "master" }, { name: "1.1.x" }], repositoryUrl, tagFormat: `v\${version}` };
  const options = {
    ...config,
    verifyConditions,
    analyzeCommits,
    verifyRelease,
    addChannel,
    generateNotes,
    prepare,
    publish,
    success,
    fail,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "1.1.x", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  const errors = [
    ...(
      await t.throwsAsync(
        semanticRelease(options, { cwd, env: {}, stdout: { write: () => {} }, stderr: { write: () => {} } })
      )
    ).errors,
  ];

  t.is(addChannel.callCount, 0);

  t.is(publish.callCount, 0);

  t.is(success.callCount, 0);

  t.is(fail.callCount, 1);
  t.deepEqual(fail.args[0][1].errors, errors);

  t.is(errors[0].code, "EINVALIDMAINTENANCEMERGE");
  t.is(errors[0].name, "SemanticReleaseError");
  t.truthy(errors[0].message);
  t.truthy(errors[0].details);
});

test.serial("Returns false value if triggered on an outdated clone", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  let { cwd, repositoryUrl } = await gitRepo(true);
  const repoDir = cwd;
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });
  cwd = await gitShallowClone(repositoryUrl);
  await gitCommits(["Third"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;

  t.false(
    await semanticRelease(
      { repositoryUrl },
      { cwd: repoDir, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() }
    )
  );
  t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [
    "The local branch master is behind the remote one, therefore a new version won't be published.",
  ]);
});

test.serial("Returns false if not running from the configured branch", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  const options = {
    branches: ["master"],
    repositoryUrl,
    verifyConditions: stub().resolves(),
    analyzeCommits: stub().resolves(),
    verifyRelease: stub().resolves(),
    generateNotes: stub().resolves(),
    addChannel: stub().resolves(),
    prepare: stub().resolves(),
    publish: stub().resolves(),
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "other-branch", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;

  t.false(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );
  t.is(
    t.context.log.args[1][0],
    "This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version won’t be published."
  );
});

test.serial("Returns false if there is no relevant changes", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const analyzeCommits = stub().resolves();
  const verifyRelease = stub().resolves();
  const generateNotes = stub().resolves();
  const publish = stub().resolves();

  const options = {
    branches: ["master"],
    repositoryUrl,
    verifyConditions: [stub().resolves()],
    analyzeCommits,
    verifyRelease,
    generateNotes,
    addChannel: stub().resolves(),
    prepare: stub().resolves(),
    publish,
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;

  t.false(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );
  t.is(analyzeCommits.callCount, 1);
  t.is(verifyRelease.callCount, 0);
  t.is(generateNotes.callCount, 0);
  t.is(publish.callCount, 0);
  t.is(
    t.context.log.args[t.context.log.args.length - 1][0],
    "There are no relevant changes, so no new version is released."
  );
});

test.serial("Exclude commits with [skip release] or [release skip] from analysis", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  const commits = await gitCommits(
    [
      "Test commit",
      "Test commit [skip release]",
      "Test commit [release skip]",
      "Test commit [Release Skip]",
      "Test commit [Skip Release]",
      "Test commit [skip    release]",
      "Test commit\n\n commit body\n[skip release]",
      "Test commit\n\n commit body\n[release skip]",
    ],
    { cwd }
  );
  await gitPush(repositoryUrl, "master", { cwd });
  const analyzeCommits = stub().resolves();
  const config = { branches: ["master"], repositoryUrl, globalOpt: "global" };
  const options = {
    ...config,
    verifyConditions: [stub().resolves(), stub().resolves()],
    analyzeCommits,
    verifyRelease: stub().resolves(),
    generateNotes: stub().resolves(),
    addChannel: stub().resolves(),
    prepare: stub().resolves(),
    publish: stub().resolves(),
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  const env = {};
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  const envCi = (await td.replaceEsm("env-ci")).default;
  td.when(envCi({ env, cwd })).thenReturn({ isCi: true, branch: "master", isPr: false });
  const semanticRelease = (await import("../index.js")).default;
  await semanticRelease(options, {
    cwd,
    env,
    stdout: new WritableStreamBuffer(),
    stderr: new WritableStreamBuffer(),
  });

  t.is(analyzeCommits.callCount, 1);
  t.is(analyzeCommits.args[0][1].commits.length, 2);
  t.deepEqual(analyzeCommits.args[0][1].commits[0], commits[commits.length - 1]);
});

test.serial('Log both plugins errors and errors thrown by "fail" plugin', async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  const pluginError = new SemanticReleaseError("Plugin error", "ERR");
  const failError1 = new Error("Fail error 1");
  const failError2 = new Error("Fail error 2");

  const options = {
    branches: ["master"],
    repositoryUrl,
    verifyConditions: stub().rejects(pluginError),
    fail: [stub().rejects(failError1), stub().rejects(failError2)],
  };
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;

  await t.throwsAsync(
    semanticRelease(options, { cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
  );

  t.is(t.context.error.args[t.context.error.args.length - 1][0], "ERR Plugin error");
  t.is(t.context.error.args[t.context.error.args.length - 3][1], failError1);
  t.is(t.context.error.args[t.context.error.args.length - 2][1], failError2);
});

test.serial('Call "fail" only if a plugin returns a SemanticReleaseError', async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  const pluginError = new Error("Plugin error");
  const fail = stub().resolves();

  const options = {
    branches: ["master"],
    repositoryUrl,
    verifyConditions: stub().rejects(pluginError),
    fail,
  };
  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;

  await t.throwsAsync(
    semanticRelease(options, { cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
  );

  t.true(fail.notCalled);
  t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
});

test.serial(
  "Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config",
  async (t) => {
    // Create a git repository, set the current working directory at the root of the repo
    const { cwd } = await gitRepo();

    await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
    await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
    const semanticRelease = (await import("../index.js")).default;
    const errors = [
      ...(
        await t.throwsAsync(
          semanticRelease({}, { cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
        )
      ).errors,
    ];

    // Verify error code and type
    t.is(errors[0].code, "ENOREPOURL");
    t.is(errors[0].name, "SemanticReleaseError");
    t.truthy(errors[0].message);
    t.truthy(errors[0].details);
  }
);

test.serial("Throw an Error if plugin returns an unexpected value", async (t) => {
  // Create a git repository, set the current working directory at the root of the repo
  const { cwd, repositoryUrl } = await gitRepo(true);
  // Add commits to the master branch
  await gitCommits(["First"], { cwd });
  // Create the tag corresponding to version 1.0.0
  await gitTagVersion("v1.0.0", undefined, { cwd });
  // Add new commits to the master branch
  await gitCommits(["Second"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const verifyConditions = stub().resolves();
  const analyzeCommits = stub().resolves("string");

  const options = {
    branches: ["master"],
    repositoryUrl,
    verifyConditions: [verifyConditions],
    analyzeCommits,
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;

  let error;
  try {
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    });
  } catch (e) {
    error = e;
  }
  t.is(error.code, "EANALYZECOMMITSOUTPUT");
  t.regex(error.details, /string/);
});

test.serial('Hide sensitive information passed to "fail" plugin', async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);

  const fail = stub().resolves();
  const env = { MY_TOKEN: "secret token" };
  const options = {
    branch: "master",
    repositoryUrl,
    verifyConditions: stub().throws(
      new SemanticReleaseError(
        `Message: Exposing token ${env.MY_TOKEN}`,
        "ERR",
        `Details: Exposing token ${env.MY_TOKEN}`
      )
    ),
    success: stub().resolves(),
    fail,
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  await t.throwsAsync(
    semanticRelease(options, { cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() })
  );

  const error = fail.args[0][1].errors[0];

  t.is(error.message, `Message: Exposing token ${SECRET_REPLACEMENT}`);
  t.is(error.details, `Details: Exposing token ${SECRET_REPLACEMENT}`);

  Object.getOwnPropertyNames(error).forEach((prop) => {
    if (isString(error[prop])) {
      t.notRegex(error[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
    }
  });
});

test.serial('Hide sensitive information passed to "success" plugin', async (t) => {
  const { cwd, repositoryUrl } = await gitRepo(true);
  await gitCommits(["feat: initial release"], { cwd });
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitCommits(["feat: new feature"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  const success = stub().resolves();
  const env = { MY_TOKEN: "secret token" };
  const options = {
    branch: "master",
    repositoryUrl,
    verifyConditions: false,
    verifyRelease: false,
    prepare: false,
    generateNotes: stub().resolves(`Exposing token ${env.MY_TOKEN}`),
    publish: stub().resolves({
      name: `Name: Exposing token ${env.MY_TOKEN}`,
      url: `URL: Exposing token ${env.MY_TOKEN}`,
    }),
    addChannel: false,
    success,
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  await semanticRelease(options, { cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer() });

  const release = success.args[0][1].releases[0];

  t.is(release.name, `Name: Exposing token ${SECRET_REPLACEMENT}`);
  t.is(release.url, `URL: Exposing token ${SECRET_REPLACEMENT}`);

  Object.getOwnPropertyNames(release).forEach((prop) => {
    if (isString(release[prop])) {
      t.notRegex(release[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
    }
  });
});

test.serial("Get all commits including the ones not in the shallow clone", async (t) => {
  let { cwd, repositoryUrl } = await gitRepo(true);
  await gitTagVersion("v1.0.0", undefined, { cwd });
  await gitCommits(["First", "Second", "Third"], { cwd });
  await gitPush(repositoryUrl, "master", { cwd });

  cwd = await gitShallowClone(repositoryUrl);

  const nextRelease = {
    name: "v2.0.0",
    type: "major",
    version: "2.0.0",
    gitHead: await getGitHead({ cwd }),
    gitTag: "v2.0.0",
    channel: undefined,
  };
  const notes = "Release notes";
  const analyzeCommits = stub().resolves(nextRelease.type);

  const config = { branches: ["master"], repositoryUrl, globalOpt: "global" };
  const options = {
    ...config,
    verifyConditions: stub().resolves(),
    analyzeCommits,
    verifyRelease: stub().resolves(),
    generateNotes: stub().resolves(notes),
    prepare: stub().resolves(),
    publish: stub().resolves(),
    success: stub().resolves(),
    fail: stub().resolves(),
  };

  await td.replaceEsm("../lib/get-logger.js", null, () => t.context.logger);
  await td.replaceEsm("env-ci", null, () => ({ isCi: true, branch: "master", isPr: false }));
  const semanticRelease = (await import("../index.js")).default;
  t.truthy(
    await semanticRelease(options, {
      cwd,
      env: {},
      stdout: new WritableStreamBuffer(),
      stderr: new WritableStreamBuffer(),
    })
  );

  t.is(analyzeCommits.args[0][1].commits.length, 3);
});
